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