github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/go/packages/packagestest/export.go (about) 1 // Copyright 2018 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 /* 6 Package packagestest creates temporary projects on disk for testing go tools on. 7 8 By changing the exporter used, you can create projects for multiple build 9 systems from the same description, and run the same tests on them in many 10 cases. 11 12 Example 13 14 As an example of packagestest use, consider the following test that runs 15 the 'go list' command on the specified modules: 16 17 // TestGoList exercises the 'go list' command in module mode and in GOPATH mode. 18 func TestGoList(t *testing.T) { packagestest.TestAll(t, testGoList) } 19 func testGoList(t *testing.T, x packagestest.Exporter) { 20 e := packagestest.Export(t, x, []packagestest.Module{ 21 { 22 Name: "gopher.example/repoa", 23 Files: map[string]interface{}{ 24 "a/a.go": "package a", 25 }, 26 }, 27 { 28 Name: "gopher.example/repob", 29 Files: map[string]interface{}{ 30 "b/b.go": "package b", 31 }, 32 }, 33 }) 34 defer e.Cleanup() 35 36 cmd := exec.Command("go", "list", "gopher.example/...") 37 cmd.Dir = e.Config.Dir 38 cmd.Env = e.Config.Env 39 out, err := cmd.Output() 40 if err != nil { 41 t.Fatal(err) 42 } 43 t.Logf("'go list gopher.example/...' with %s mode layout:\n%s", x.Name(), out) 44 } 45 46 TestGoList uses TestAll to exercise the 'go list' command with all 47 exporters known to packagestest. Currently, packagestest includes 48 exporters that produce module mode layouts and GOPATH mode layouts. 49 Running the test with verbose output will print: 50 51 === RUN TestGoList 52 === RUN TestGoList/GOPATH 53 === RUN TestGoList/Modules 54 --- PASS: TestGoList (0.21s) 55 --- PASS: TestGoList/GOPATH (0.03s) 56 main_test.go:36: 'go list gopher.example/...' with GOPATH mode layout: 57 gopher.example/repoa/a 58 gopher.example/repob/b 59 --- PASS: TestGoList/Modules (0.18s) 60 main_test.go:36: 'go list gopher.example/...' with Modules mode layout: 61 gopher.example/repoa/a 62 gopher.example/repob/b 63 64 */ 65 package packagestest 66 67 import ( 68 "errors" 69 "flag" 70 "fmt" 71 "go/token" 72 "io" 73 "io/ioutil" 74 "log" 75 "os" 76 "path/filepath" 77 "runtime" 78 "strings" 79 "testing" 80 81 "github.com/powerman/golang-tools/go/expect" 82 "github.com/powerman/golang-tools/go/packages" 83 "github.com/powerman/golang-tools/internal/span" 84 "github.com/powerman/golang-tools/internal/testenv" 85 "golang.org/x/xerrors" 86 ) 87 88 var ( 89 skipCleanup = flag.Bool("skip-cleanup", false, "Do not delete the temporary export folders") // for debugging 90 ) 91 92 // ErrUnsupported indicates an error due to an operation not supported on the 93 // current platform. 94 var ErrUnsupported = errors.New("operation is not supported") 95 96 // Module is a representation of a go module. 97 type Module struct { 98 // Name is the base name of the module as it would be in the go.mod file. 99 Name string 100 // Files is the set of source files for all packages that make up the module. 101 // The keys are the file fragment that follows the module name, the value can 102 // be a string or byte slice, in which case it is the contents of the 103 // file, otherwise it must be a Writer function. 104 Files map[string]interface{} 105 106 // Overlay is the set of source file overlays for the module. 107 // The keys are the file fragment as in the Files configuration. 108 // The values are the in memory overlay content for the file. 109 Overlay map[string][]byte 110 } 111 112 // A Writer is a function that writes out a test file. 113 // It is provided the name of the file to write, and may return an error if it 114 // cannot write the file. 115 // These are used as the content of the Files map in a Module. 116 type Writer func(filename string) error 117 118 // Exported is returned by the Export function to report the structure that was produced on disk. 119 type Exported struct { 120 // Config is a correctly configured packages.Config ready to be passed to packages.Load. 121 // Exactly what it will contain varies depending on the Exporter being used. 122 Config *packages.Config 123 124 // Modules is the module description that was used to produce this exported data set. 125 Modules []Module 126 127 ExpectFileSet *token.FileSet // The file set used when parsing expectations 128 129 Exporter Exporter // the exporter used 130 temp string // the temporary directory that was exported to 131 primary string // the first non GOROOT module that was exported 132 written map[string]map[string]string // the full set of exported files 133 notes []*expect.Note // The list of expectations extracted from go source files 134 markers map[string]span.Range // The set of markers extracted from go source files 135 } 136 137 // Exporter implementations are responsible for converting from the generic description of some 138 // test data to a driver specific file layout. 139 type Exporter interface { 140 // Name reports the name of the exporter, used in logging and sub-test generation. 141 Name() string 142 // Filename reports the system filename for test data source file. 143 // It is given the base directory, the module the file is part of and the filename fragment to 144 // work from. 145 Filename(exported *Exported, module, fragment string) string 146 // Finalize is called once all files have been written to write any extra data needed and modify 147 // the Config to match. It is handed the full list of modules that were encountered while writing 148 // files. 149 Finalize(exported *Exported) error 150 } 151 152 // All is the list of known exporters. 153 // This is used by TestAll to run tests with all the exporters. 154 var All []Exporter 155 156 // TestAll invokes the testing function once for each exporter registered in 157 // the All global. 158 // Each exporter will be run as a sub-test named after the exporter being used. 159 func TestAll(t *testing.T, f func(*testing.T, Exporter)) { 160 t.Helper() 161 for _, e := range All { 162 e := e // in case f calls t.Parallel 163 t.Run(e.Name(), func(t *testing.T) { 164 t.Helper() 165 f(t, e) 166 }) 167 } 168 } 169 170 // BenchmarkAll invokes the testing function once for each exporter registered in 171 // the All global. 172 // Each exporter will be run as a sub-test named after the exporter being used. 173 func BenchmarkAll(b *testing.B, f func(*testing.B, Exporter)) { 174 b.Helper() 175 for _, e := range All { 176 e := e // in case f calls t.Parallel 177 b.Run(e.Name(), func(b *testing.B) { 178 b.Helper() 179 f(b, e) 180 }) 181 } 182 } 183 184 // Export is called to write out a test directory from within a test function. 185 // It takes the exporter and the build system agnostic module descriptions, and 186 // uses them to build a temporary directory. 187 // It returns an Exported with the results of the export. 188 // The Exported.Config is prepared for loading from the exported data. 189 // You must invoke Exported.Cleanup on the returned value to clean up. 190 // The file deletion in the cleanup can be skipped by setting the skip-cleanup 191 // flag when invoking the test, allowing the temporary directory to be left for 192 // debugging tests. 193 // 194 // If the Writer for any file within any module returns an error equivalent to 195 // ErrUnspported, Export skips the test. 196 func Export(t testing.TB, exporter Exporter, modules []Module) *Exported { 197 t.Helper() 198 if exporter == Modules { 199 testenv.NeedsTool(t, "go") 200 } 201 202 dirname := strings.Replace(t.Name(), "/", "_", -1) 203 dirname = strings.Replace(dirname, "#", "_", -1) // duplicate subtests get a #NNN suffix. 204 temp, err := ioutil.TempDir("", dirname) 205 if err != nil { 206 t.Fatal(err) 207 } 208 exported := &Exported{ 209 Config: &packages.Config{ 210 Dir: temp, 211 Env: append(os.Environ(), "GOPACKAGESDRIVER=off", "GOROOT="), // Clear GOROOT to work around #32849. 212 Overlay: make(map[string][]byte), 213 Tests: true, 214 Mode: packages.LoadImports, 215 }, 216 Modules: modules, 217 Exporter: exporter, 218 temp: temp, 219 primary: modules[0].Name, 220 written: map[string]map[string]string{}, 221 ExpectFileSet: token.NewFileSet(), 222 } 223 defer func() { 224 if t.Failed() || t.Skipped() { 225 exported.Cleanup() 226 } 227 }() 228 for _, module := range modules { 229 // Create all parent directories before individual files. If any file is a 230 // symlink to a directory, that directory must exist before the symlink is 231 // created or else it may be created with the wrong type on Windows. 232 // (See https://golang.org/issue/39183.) 233 for fragment := range module.Files { 234 fullpath := exporter.Filename(exported, module.Name, filepath.FromSlash(fragment)) 235 if err := os.MkdirAll(filepath.Dir(fullpath), 0755); err != nil { 236 t.Fatal(err) 237 } 238 } 239 240 for fragment, value := range module.Files { 241 fullpath := exporter.Filename(exported, module.Name, filepath.FromSlash(fragment)) 242 written, ok := exported.written[module.Name] 243 if !ok { 244 written = map[string]string{} 245 exported.written[module.Name] = written 246 } 247 written[fragment] = fullpath 248 switch value := value.(type) { 249 case Writer: 250 if err := value(fullpath); err != nil { 251 if xerrors.Is(err, ErrUnsupported) { 252 t.Skip(err) 253 } 254 t.Fatal(err) 255 } 256 case string: 257 if err := ioutil.WriteFile(fullpath, []byte(value), 0644); err != nil { 258 t.Fatal(err) 259 } 260 default: 261 t.Fatalf("Invalid type %T in files, must be string or Writer", value) 262 } 263 } 264 for fragment, value := range module.Overlay { 265 fullpath := exporter.Filename(exported, module.Name, filepath.FromSlash(fragment)) 266 exported.Config.Overlay[fullpath] = value 267 } 268 } 269 if err := exporter.Finalize(exported); err != nil { 270 t.Fatal(err) 271 } 272 testenv.NeedsGoPackagesEnv(t, exported.Config.Env) 273 return exported 274 } 275 276 // Script returns a Writer that writes out contents to the file and sets the 277 // executable bit on the created file. 278 // It is intended for source files that are shell scripts. 279 func Script(contents string) Writer { 280 return func(filename string) error { 281 return ioutil.WriteFile(filename, []byte(contents), 0755) 282 } 283 } 284 285 // Link returns a Writer that creates a hard link from the specified source to 286 // the required file. 287 // This is used to link testdata files into the generated testing tree. 288 // 289 // If hard links to source are not supported on the destination filesystem, the 290 // returned Writer returns an error for which errors.Is(_, ErrUnsupported) 291 // returns true. 292 func Link(source string) Writer { 293 return func(filename string) error { 294 linkErr := os.Link(source, filename) 295 296 if linkErr != nil && !builderMustSupportLinks() { 297 // Probe to figure out whether Link failed because the Link operation 298 // isn't supported. 299 if stat, err := openAndStat(source); err == nil { 300 if err := createEmpty(filename, stat.Mode()); err == nil { 301 // Successfully opened the source and created the destination, 302 // but the result is empty and not a hard-link. 303 return &os.PathError{Op: "Link", Path: filename, Err: ErrUnsupported} 304 } 305 } 306 } 307 308 return linkErr 309 } 310 } 311 312 // Symlink returns a Writer that creates a symlink from the specified source to the 313 // required file. 314 // This is used to link testdata files into the generated testing tree. 315 // 316 // If symlinks to source are not supported on the destination filesystem, the 317 // returned Writer returns an error for which errors.Is(_, ErrUnsupported) 318 // returns true. 319 func Symlink(source string) Writer { 320 if !strings.HasPrefix(source, ".") { 321 if absSource, err := filepath.Abs(source); err == nil { 322 if _, err := os.Stat(source); !os.IsNotExist(err) { 323 source = absSource 324 } 325 } 326 } 327 return func(filename string) error { 328 symlinkErr := os.Symlink(source, filename) 329 330 if symlinkErr != nil && !builderMustSupportLinks() { 331 // Probe to figure out whether Symlink failed because the Symlink 332 // operation isn't supported. 333 fullSource := source 334 if !filepath.IsAbs(source) { 335 // Compute the target path relative to the parent of filename, not the 336 // current working directory. 337 fullSource = filepath.Join(filename, "..", source) 338 } 339 stat, err := openAndStat(fullSource) 340 mode := os.ModePerm 341 if err == nil { 342 mode = stat.Mode() 343 } else if !xerrors.Is(err, os.ErrNotExist) { 344 // We couldn't open the source, but it might exist. We don't expect to be 345 // able to portably create a symlink to a file we can't see. 346 return symlinkErr 347 } 348 349 if err := createEmpty(filename, mode|0644); err == nil { 350 // Successfully opened the source (or verified that it does not exist) and 351 // created the destination, but we couldn't create it as a symlink. 352 // Probably the OS just doesn't support symlinks in this context. 353 return &os.PathError{Op: "Symlink", Path: filename, Err: ErrUnsupported} 354 } 355 } 356 357 return symlinkErr 358 } 359 } 360 361 // builderMustSupportLinks reports whether we are running on a Go builder 362 // that is known to support hard and symbolic links. 363 func builderMustSupportLinks() bool { 364 if os.Getenv("GO_BUILDER_NAME") == "" { 365 // Any OS can be configured to mount an exotic filesystem. 366 // Don't make assumptions about what users are running. 367 return false 368 } 369 370 switch runtime.GOOS { 371 case "windows", "plan9": 372 // Some versions of Windows and all versions of plan9 do not support 373 // symlinks by default. 374 return false 375 376 default: 377 // All other platforms should support symlinks by default, and our builders 378 // should not do anything unusual that would violate that. 379 return true 380 } 381 } 382 383 // openAndStat attempts to open source for reading. 384 func openAndStat(source string) (os.FileInfo, error) { 385 src, err := os.Open(source) 386 if err != nil { 387 return nil, err 388 } 389 stat, err := src.Stat() 390 src.Close() 391 if err != nil { 392 return nil, err 393 } 394 return stat, nil 395 } 396 397 // createEmpty creates an empty file or directory (depending on mode) 398 // at dst, with the same permissions as mode. 399 func createEmpty(dst string, mode os.FileMode) error { 400 if mode.IsDir() { 401 return os.Mkdir(dst, mode.Perm()) 402 } 403 404 f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode.Perm()) 405 if err != nil { 406 return err 407 } 408 if err := f.Close(); err != nil { 409 os.Remove(dst) // best-effort 410 return err 411 } 412 413 return nil 414 } 415 416 // Copy returns a Writer that copies a file from the specified source to the 417 // required file. 418 // This is used to copy testdata files into the generated testing tree. 419 func Copy(source string) Writer { 420 return func(filename string) error { 421 stat, err := os.Stat(source) 422 if err != nil { 423 return err 424 } 425 if !stat.Mode().IsRegular() { 426 // cannot copy non-regular files (e.g., directories, 427 // symlinks, devices, etc.) 428 return fmt.Errorf("cannot copy non regular file %s", source) 429 } 430 return copyFile(filename, source, stat.Mode().Perm()) 431 } 432 } 433 434 func copyFile(dest, source string, perm os.FileMode) error { 435 src, err := os.Open(source) 436 if err != nil { 437 return err 438 } 439 defer src.Close() 440 441 dst, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm) 442 if err != nil { 443 return err 444 } 445 446 _, err = io.Copy(dst, src) 447 if closeErr := dst.Close(); err == nil { 448 err = closeErr 449 } 450 return err 451 } 452 453 // GroupFilesByModules attempts to map directories to the modules within each directory. 454 // This function assumes that the folder is structured in the following way: 455 // - dir 456 // - primarymod 457 // - .go files 458 // - packages 459 // - go.mod (optional) 460 // - modules 461 // - repoa 462 // - mod1 463 // - .go files 464 // - packages 465 // - go.mod (optional) 466 // It scans the directory tree anchored at root and adds a Copy writer to the 467 // map for every file found. 468 // This is to enable the common case in tests where you have a full copy of the 469 // package in your testdata. 470 func GroupFilesByModules(root string) ([]Module, error) { 471 root = filepath.FromSlash(root) 472 primarymodPath := filepath.Join(root, "primarymod") 473 474 _, err := os.Stat(primarymodPath) 475 if os.IsNotExist(err) { 476 return nil, fmt.Errorf("could not find primarymod folder within %s", root) 477 } 478 479 primarymod := &Module{ 480 Name: root, 481 Files: make(map[string]interface{}), 482 Overlay: make(map[string][]byte), 483 } 484 mods := map[string]*Module{ 485 root: primarymod, 486 } 487 modules := []Module{*primarymod} 488 489 if err := filepath.Walk(primarymodPath, func(path string, info os.FileInfo, err error) error { 490 if err != nil { 491 return err 492 } 493 if info.IsDir() { 494 return nil 495 } 496 fragment, err := filepath.Rel(primarymodPath, path) 497 if err != nil { 498 return err 499 } 500 primarymod.Files[filepath.ToSlash(fragment)] = Copy(path) 501 return nil 502 }); err != nil { 503 return nil, err 504 } 505 506 modulesPath := filepath.Join(root, "modules") 507 if _, err := os.Stat(modulesPath); os.IsNotExist(err) { 508 return modules, nil 509 } 510 511 var currentRepo, currentModule string 512 updateCurrentModule := func(dir string) { 513 if dir == currentModule { 514 return 515 } 516 // Handle the case where we step into a nested directory that is a module 517 // and then step out into the parent which is also a module. 518 // Example: 519 // - repoa 520 // - moda 521 // - go.mod 522 // - v2 523 // - go.mod 524 // - what.go 525 // - modb 526 for dir != root { 527 if mods[dir] != nil { 528 currentModule = dir 529 return 530 } 531 dir = filepath.Dir(dir) 532 } 533 } 534 535 if err := filepath.Walk(modulesPath, func(path string, info os.FileInfo, err error) error { 536 if err != nil { 537 return err 538 } 539 enclosingDir := filepath.Dir(path) 540 // If the path is not a directory, then we want to add the path to 541 // the files map of the currentModule. 542 if !info.IsDir() { 543 updateCurrentModule(enclosingDir) 544 fragment, err := filepath.Rel(currentModule, path) 545 if err != nil { 546 return err 547 } 548 mods[currentModule].Files[filepath.ToSlash(fragment)] = Copy(path) 549 return nil 550 } 551 // If the path is a directory and it's enclosing folder is equal to 552 // the modules folder, then the path is a new repo. 553 if enclosingDir == modulesPath { 554 currentRepo = path 555 return nil 556 } 557 // If the path is a directory and it's enclosing folder is not the same 558 // as the current repo and it is not of the form `v1`,`v2`,... 559 // then the path is a folder/package of the current module. 560 if enclosingDir != currentRepo && !versionSuffixRE.MatchString(filepath.Base(path)) { 561 return nil 562 } 563 // If the path is a directory and it's enclosing folder is the current repo 564 // then the path is a new module. 565 module, err := filepath.Rel(modulesPath, path) 566 if err != nil { 567 return err 568 } 569 mods[path] = &Module{ 570 Name: filepath.ToSlash(module), 571 Files: make(map[string]interface{}), 572 Overlay: make(map[string][]byte), 573 } 574 currentModule = path 575 modules = append(modules, *mods[path]) 576 return nil 577 }); err != nil { 578 return nil, err 579 } 580 return modules, nil 581 } 582 583 // MustCopyFileTree returns a file set for a module based on a real directory tree. 584 // It scans the directory tree anchored at root and adds a Copy writer to the 585 // map for every file found. It skips copying files in nested modules. 586 // This is to enable the common case in tests where you have a full copy of the 587 // package in your testdata. 588 // This will panic if there is any kind of error trying to walk the file tree. 589 func MustCopyFileTree(root string) map[string]interface{} { 590 result := map[string]interface{}{} 591 if err := filepath.Walk(filepath.FromSlash(root), func(path string, info os.FileInfo, err error) error { 592 if err != nil { 593 return err 594 } 595 if info.IsDir() { 596 // skip nested modules. 597 if path != root { 598 if fi, err := os.Stat(filepath.Join(path, "go.mod")); err == nil && !fi.IsDir() { 599 return filepath.SkipDir 600 } 601 } 602 return nil 603 } 604 fragment, err := filepath.Rel(root, path) 605 if err != nil { 606 return err 607 } 608 result[filepath.ToSlash(fragment)] = Copy(path) 609 return nil 610 }); err != nil { 611 log.Panic(fmt.Sprintf("MustCopyFileTree failed: %v", err)) 612 } 613 return result 614 } 615 616 // Cleanup removes the temporary directory (unless the --skip-cleanup flag was set) 617 // It is safe to call cleanup multiple times. 618 func (e *Exported) Cleanup() { 619 if e.temp == "" { 620 return 621 } 622 if *skipCleanup { 623 log.Printf("Skipping cleanup of temp dir: %s", e.temp) 624 return 625 } 626 // Make everything read-write so that the Module exporter's module cache can be deleted. 627 filepath.Walk(e.temp, func(path string, info os.FileInfo, err error) error { 628 if err != nil { 629 return nil 630 } 631 if info.IsDir() { 632 os.Chmod(path, 0777) 633 } 634 return nil 635 }) 636 os.RemoveAll(e.temp) // ignore errors 637 e.temp = "" 638 } 639 640 // Temp returns the temporary directory that was generated. 641 func (e *Exported) Temp() string { 642 return e.temp 643 } 644 645 // File returns the full path for the given module and file fragment. 646 func (e *Exported) File(module, fragment string) string { 647 if m := e.written[module]; m != nil { 648 return m[fragment] 649 } 650 return "" 651 } 652 653 // FileContents returns the contents of the specified file. 654 // It will use the overlay if the file is present, otherwise it will read it 655 // from disk. 656 func (e *Exported) FileContents(filename string) ([]byte, error) { 657 if content, found := e.Config.Overlay[filename]; found { 658 return content, nil 659 } 660 content, err := ioutil.ReadFile(filename) 661 if err != nil { 662 return nil, err 663 } 664 return content, nil 665 }