|  | // Copyright 2018 The Go Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style | 
|  | // license that can be found in the LICENSE file. | 
|  |  | 
|  | /* | 
|  | Package packagestest creates temporary projects on disk for testing go tools on. | 
|  |  | 
|  | By changing the exporter used, you can create projects for multiple build | 
|  | systems from the same description, and run the same tests on them in many | 
|  | cases. | 
|  |  | 
|  | Example | 
|  |  | 
|  | As an example of packagestest use, consider the following test that runs | 
|  | the 'go list' command on the specified modules: | 
|  |  | 
|  | // TestGoList exercises the 'go list' command in module mode and in GOPATH mode. | 
|  | func TestGoList(t *testing.T) { packagestest.TestAll(t, testGoList) } | 
|  | func testGoList(t *testing.T, x packagestest.Exporter) { | 
|  | e := packagestest.Export(t, x, []packagestest.Module{ | 
|  | { | 
|  | Name: "gopher.example/repoa", | 
|  | Files: map[string]interface{}{ | 
|  | "a/a.go": "package a", | 
|  | }, | 
|  | }, | 
|  | { | 
|  | Name: "gopher.example/repob", | 
|  | Files: map[string]interface{}{ | 
|  | "b/b.go": "package b", | 
|  | }, | 
|  | }, | 
|  | }) | 
|  | defer e.Cleanup() | 
|  |  | 
|  | cmd := exec.Command("go", "list", "gopher.example/...") | 
|  | cmd.Dir = e.Config.Dir | 
|  | cmd.Env = e.Config.Env | 
|  | out, err := cmd.Output() | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | t.Logf("'go list gopher.example/...' with %s mode layout:\n%s", x.Name(), out) | 
|  | } | 
|  |  | 
|  | TestGoList uses TestAll to exercise the 'go list' command with all | 
|  | exporters known to packagestest. Currently, packagestest includes | 
|  | exporters that produce module mode layouts and GOPATH mode layouts. | 
|  | Running the test with verbose output will print: | 
|  |  | 
|  | === RUN   TestGoList | 
|  | === RUN   TestGoList/GOPATH | 
|  | === RUN   TestGoList/Modules | 
|  | --- PASS: TestGoList (0.21s) | 
|  | --- PASS: TestGoList/GOPATH (0.03s) | 
|  | main_test.go:36: 'go list gopher.example/...' with GOPATH mode layout: | 
|  | gopher.example/repoa/a | 
|  | gopher.example/repob/b | 
|  | --- PASS: TestGoList/Modules (0.18s) | 
|  | main_test.go:36: 'go list gopher.example/...' with Modules mode layout: | 
|  | gopher.example/repoa/a | 
|  | gopher.example/repob/b | 
|  |  | 
|  | */ | 
|  | package packagestest | 
|  |  | 
|  | import ( | 
|  | "flag" | 
|  | "fmt" | 
|  | "go/token" | 
|  | "io/ioutil" | 
|  | "log" | 
|  | "os" | 
|  | "path/filepath" | 
|  | "strings" | 
|  | "testing" | 
|  |  | 
|  | "golang.org/x/tools/go/expect" | 
|  | "golang.org/x/tools/go/packages" | 
|  | "golang.org/x/tools/internal/span" | 
|  | "golang.org/x/tools/internal/testenv" | 
|  | ) | 
|  |  | 
|  | var ( | 
|  | skipCleanup = flag.Bool("skip-cleanup", false, "Do not delete the temporary export folders") // for debugging | 
|  | ) | 
|  |  | 
|  | // Module is a representation of a go module. | 
|  | type Module struct { | 
|  | // Name is the base name of the module as it would be in the go.mod file. | 
|  | Name string | 
|  | // Files is the set of source files for all packages that make up the module. | 
|  | // The keys are the file fragment that follows the module name, the value can | 
|  | // be a string or byte slice, in which case it is the contents of the | 
|  | // file, otherwise it must be a Writer function. | 
|  | Files map[string]interface{} | 
|  |  | 
|  | // Overlay is the set of source file overlays for the module. | 
|  | // The keys are the file fragment as in the Files configuration. | 
|  | // The values are the in memory overlay content for the file. | 
|  | Overlay map[string][]byte | 
|  | } | 
|  |  | 
|  | // A Writer is a function that writes out a test file. | 
|  | // It is provided the name of the file to write, and may return an error if it | 
|  | // cannot write the file. | 
|  | // These are used as the content of the Files map in a Module. | 
|  | type Writer func(filename string) error | 
|  |  | 
|  | // Exported is returned by the Export function to report the structure that was produced on disk. | 
|  | type Exported struct { | 
|  | // Config is a correctly configured packages.Config ready to be passed to packages.Load. | 
|  | // Exactly what it will contain varies depending on the Exporter being used. | 
|  | Config *packages.Config | 
|  |  | 
|  | // Modules is the module description that was used to produce this exported data set. | 
|  | Modules []Module | 
|  |  | 
|  | ExpectFileSet *token.FileSet // The file set used when parsing expectations | 
|  |  | 
|  | Exporter Exporter                     // the exporter used | 
|  | temp     string                       // the temporary directory that was exported to | 
|  | primary  string                       // the first non GOROOT module that was exported | 
|  | written  map[string]map[string]string // the full set of exported files | 
|  | notes    []*expect.Note               // The list of expectations extracted from go source files | 
|  | markers  map[string]span.Range        // The set of markers extracted from go source files | 
|  | } | 
|  |  | 
|  | // Exporter implementations are responsible for converting from the generic description of some | 
|  | // test data to a driver specific file layout. | 
|  | type Exporter interface { | 
|  | // Name reports the name of the exporter, used in logging and sub-test generation. | 
|  | Name() string | 
|  | // Filename reports the system filename for test data source file. | 
|  | // It is given the base directory, the module the file is part of and the filename fragment to | 
|  | // work from. | 
|  | Filename(exported *Exported, module, fragment string) string | 
|  | // Finalize is called once all files have been written to write any extra data needed and modify | 
|  | // the Config to match. It is handed the full list of modules that were encountered while writing | 
|  | // files. | 
|  | Finalize(exported *Exported) error | 
|  | } | 
|  |  | 
|  | // All is the list of known exporters. | 
|  | // This is used by TestAll to run tests with all the exporters. | 
|  | var All []Exporter | 
|  |  | 
|  | // TestAll invokes the testing function once for each exporter registered in | 
|  | // the All global. | 
|  | // Each exporter will be run as a sub-test named after the exporter being used. | 
|  | func TestAll(t *testing.T, f func(*testing.T, Exporter)) { | 
|  | t.Helper() | 
|  | for _, e := range All { | 
|  | t.Run(e.Name(), func(t *testing.T) { | 
|  | t.Helper() | 
|  | f(t, e) | 
|  | }) | 
|  | } | 
|  | } | 
|  |  | 
|  | // BenchmarkAll invokes the testing function once for each exporter registered in | 
|  | // the All global. | 
|  | // Each exporter will be run as a sub-test named after the exporter being used. | 
|  | func BenchmarkAll(b *testing.B, f func(*testing.B, Exporter)) { | 
|  | b.Helper() | 
|  | for _, e := range All { | 
|  | b.Run(e.Name(), func(b *testing.B) { | 
|  | b.Helper() | 
|  | f(b, e) | 
|  | }) | 
|  | } | 
|  | } | 
|  |  | 
|  | // Export is called to write out a test directory from within a test function. | 
|  | // It takes the exporter and the build system agnostic module descriptions, and | 
|  | // uses them to build a temporary directory. | 
|  | // It returns an Exported with the results of the export. | 
|  | // The Exported.Config is prepared for loading from the exported data. | 
|  | // You must invoke Exported.Cleanup on the returned value to clean up. | 
|  | // The file deletion in the cleanup can be skipped by setting the skip-cleanup | 
|  | // flag when invoking the test, allowing the temporary directory to be left for | 
|  | // debugging tests. | 
|  | func Export(t testing.TB, exporter Exporter, modules []Module) *Exported { | 
|  | t.Helper() | 
|  | if exporter == Modules { | 
|  | testenv.NeedsTool(t, "go") | 
|  | } | 
|  |  | 
|  | dirname := strings.Replace(t.Name(), "/", "_", -1) | 
|  | dirname = strings.Replace(dirname, "#", "_", -1) // duplicate subtests get a #NNN suffix. | 
|  | temp, err := ioutil.TempDir("", dirname) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | exported := &Exported{ | 
|  | Config: &packages.Config{ | 
|  | Dir:     temp, | 
|  | Env:     append(os.Environ(), "GOPACKAGESDRIVER=off", "GOROOT="), // Clear GOROOT to work around #32849. | 
|  | Overlay: make(map[string][]byte), | 
|  | Tests:   true, | 
|  | Mode:    packages.LoadImports, | 
|  | }, | 
|  | Modules:       modules, | 
|  | Exporter:      exporter, | 
|  | temp:          temp, | 
|  | primary:       modules[0].Name, | 
|  | written:       map[string]map[string]string{}, | 
|  | ExpectFileSet: token.NewFileSet(), | 
|  | } | 
|  | defer func() { | 
|  | if t.Failed() || t.Skipped() { | 
|  | exported.Cleanup() | 
|  | } | 
|  | }() | 
|  | for _, module := range modules { | 
|  | for fragment, value := range module.Files { | 
|  | fullpath := exporter.Filename(exported, module.Name, filepath.FromSlash(fragment)) | 
|  | written, ok := exported.written[module.Name] | 
|  | if !ok { | 
|  | written = map[string]string{} | 
|  | exported.written[module.Name] = written | 
|  | } | 
|  | written[fragment] = fullpath | 
|  | if err := os.MkdirAll(filepath.Dir(fullpath), 0755); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | switch value := value.(type) { | 
|  | case Writer: | 
|  | if err := value(fullpath); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | case string: | 
|  | if err := ioutil.WriteFile(fullpath, []byte(value), 0644); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | default: | 
|  | t.Fatalf("Invalid type %T in files, must be string or Writer", value) | 
|  | } | 
|  | } | 
|  | for fragment, value := range module.Overlay { | 
|  | fullpath := exporter.Filename(exported, module.Name, filepath.FromSlash(fragment)) | 
|  | exported.Config.Overlay[fullpath] = value | 
|  | } | 
|  | } | 
|  | if err := exporter.Finalize(exported); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | testenv.NeedsGoPackagesEnv(t, exported.Config.Env) | 
|  | return exported | 
|  | } | 
|  |  | 
|  | // Script returns a Writer that writes out contents to the file and sets the | 
|  | // executable bit on the created file. | 
|  | // It is intended for source files that are shell scripts. | 
|  | func Script(contents string) Writer { | 
|  | return func(filename string) error { | 
|  | return ioutil.WriteFile(filename, []byte(contents), 0755) | 
|  | } | 
|  | } | 
|  |  | 
|  | // Link returns a Writer that creates a hard link from the specified source to | 
|  | // the required file. | 
|  | // This is used to link testdata files into the generated testing tree. | 
|  | func Link(source string) Writer { | 
|  | return func(filename string) error { | 
|  | return os.Link(source, filename) | 
|  | } | 
|  | } | 
|  |  | 
|  | // Symlink returns a Writer that creates a symlink from the specified source to the | 
|  | // required file. | 
|  | // This is used to link testdata files into the generated testing tree. | 
|  | func Symlink(source string) Writer { | 
|  | if !strings.HasPrefix(source, ".") { | 
|  | if abspath, err := filepath.Abs(source); err == nil { | 
|  | if _, err := os.Stat(source); !os.IsNotExist(err) { | 
|  | source = abspath | 
|  | } | 
|  | } | 
|  | } | 
|  | return func(filename string) error { | 
|  | return os.Symlink(source, filename) | 
|  | } | 
|  | } | 
|  |  | 
|  | // Copy returns a Writer that copies a file from the specified source to the | 
|  | // required file. | 
|  | // This is used to copy testdata files into the generated testing tree. | 
|  | func Copy(source string) Writer { | 
|  | return func(filename string) error { | 
|  | stat, err := os.Stat(source) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | if !stat.Mode().IsRegular() { | 
|  | // cannot copy non-regular files (e.g., directories, | 
|  | // symlinks, devices, etc.) | 
|  | return fmt.Errorf("cannot copy non regular file %s", source) | 
|  | } | 
|  | contents, err := ioutil.ReadFile(source) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | return ioutil.WriteFile(filename, contents, stat.Mode()) | 
|  | } | 
|  | } | 
|  |  | 
|  | // GroupFilesByModules attempts to map directories to the modules within each directory. | 
|  | // This function assumes that the folder is structured in the following way: | 
|  | // - dir | 
|  | //   - primarymod | 
|  | //     - .go files | 
|  | //		 - packages | 
|  | //		 - go.mod (optional) | 
|  | //	 - modules | 
|  | // 		 - repoa | 
|  | //		   - mod1 | 
|  | //	       - .go files | 
|  | //			   -  packages | 
|  | //		  	 - go.mod (optional) | 
|  | // It scans the directory tree anchored at root and adds a Copy writer to the | 
|  | // map for every file found. | 
|  | // This is to enable the common case in tests where you have a full copy of the | 
|  | // package in your testdata. | 
|  | func GroupFilesByModules(root string) ([]Module, error) { | 
|  | root = filepath.FromSlash(root) | 
|  | primarymodPath := filepath.Join(root, "primarymod") | 
|  |  | 
|  | _, err := os.Stat(primarymodPath) | 
|  | if os.IsNotExist(err) { | 
|  | return nil, fmt.Errorf("could not find primarymod folder within %s", root) | 
|  | } | 
|  |  | 
|  | primarymod := &Module{ | 
|  | Name:    root, | 
|  | Files:   make(map[string]interface{}), | 
|  | Overlay: make(map[string][]byte), | 
|  | } | 
|  | mods := map[string]*Module{ | 
|  | root: primarymod, | 
|  | } | 
|  | modules := []Module{*primarymod} | 
|  |  | 
|  | if err := filepath.Walk(primarymodPath, func(path string, info os.FileInfo, err error) error { | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | if info.IsDir() { | 
|  | return nil | 
|  | } | 
|  | fragment, err := filepath.Rel(primarymodPath, path) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | primarymod.Files[filepath.ToSlash(fragment)] = Copy(path) | 
|  | return nil | 
|  | }); err != nil { | 
|  | return nil, err | 
|  | } | 
|  |  | 
|  | modulesPath := filepath.Join(root, "modules") | 
|  | if _, err := os.Stat(modulesPath); os.IsNotExist(err) { | 
|  | return modules, nil | 
|  | } | 
|  |  | 
|  | var currentRepo, currentModule string | 
|  | updateCurrentModule := func(dir string) { | 
|  | if dir == currentModule { | 
|  | return | 
|  | } | 
|  | // Handle the case where we step into a nested directory that is a module | 
|  | // and then step out into the parent which is also a module. | 
|  | // Example: | 
|  | // - repoa | 
|  | //   - moda | 
|  | //     - go.mod | 
|  | //     - v2 | 
|  | //       - go.mod | 
|  | //     - what.go | 
|  | //   - modb | 
|  | for dir != root { | 
|  | if mods[dir] != nil { | 
|  | currentModule = dir | 
|  | return | 
|  | } | 
|  | dir = filepath.Dir(dir) | 
|  | } | 
|  | } | 
|  |  | 
|  | if err := filepath.Walk(modulesPath, func(path string, info os.FileInfo, err error) error { | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | enclosingDir := filepath.Dir(path) | 
|  | // If the path is not a directory, then we want to add the path to | 
|  | // the files map of the currentModule. | 
|  | if !info.IsDir() { | 
|  | updateCurrentModule(enclosingDir) | 
|  | fragment, err := filepath.Rel(currentModule, path) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | mods[currentModule].Files[filepath.ToSlash(fragment)] = Copy(path) | 
|  | return nil | 
|  | } | 
|  | // If the path is a directory and it's enclosing folder is equal to | 
|  | // the modules folder, then the path is a new repo. | 
|  | if enclosingDir == modulesPath { | 
|  | currentRepo = path | 
|  | return nil | 
|  | } | 
|  | // If the path is a directory and it's enclosing folder is not the same | 
|  | // as the current repo and it is not of the form `v1`,`v2`,... | 
|  | // then the path is a folder/package of the current module. | 
|  | if enclosingDir != currentRepo && !versionSuffixRE.MatchString(filepath.Base(path)) { | 
|  | return nil | 
|  | } | 
|  | // If the path is a directory and it's enclosing folder is the current repo | 
|  | // then the path is a new module. | 
|  | module, err := filepath.Rel(modulesPath, path) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | mods[path] = &Module{ | 
|  | Name:    filepath.ToSlash(module), | 
|  | Files:   make(map[string]interface{}), | 
|  | Overlay: make(map[string][]byte), | 
|  | } | 
|  | currentModule = path | 
|  | modules = append(modules, *mods[path]) | 
|  | return nil | 
|  | }); err != nil { | 
|  | return nil, err | 
|  | } | 
|  | return modules, nil | 
|  | } | 
|  |  | 
|  | // MustCopyFileTree returns a file set for a module based on a real directory tree. | 
|  | // It scans the directory tree anchored at root and adds a Copy writer to the | 
|  | // map for every file found. | 
|  | // This is to enable the common case in tests where you have a full copy of the | 
|  | // package in your testdata. | 
|  | // This will panic if there is any kind of error trying to walk the file tree. | 
|  | func MustCopyFileTree(root string) map[string]interface{} { | 
|  | result := map[string]interface{}{} | 
|  | if err := filepath.Walk(filepath.FromSlash(root), func(path string, info os.FileInfo, err error) error { | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | if info.IsDir() { | 
|  | return nil | 
|  | } | 
|  | fragment, err := filepath.Rel(root, path) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | result[filepath.ToSlash(fragment)] = Copy(path) | 
|  | return nil | 
|  | }); err != nil { | 
|  | log.Panic(fmt.Sprintf("MustCopyFileTree failed: %v", err)) | 
|  | } | 
|  | return result | 
|  | } | 
|  |  | 
|  | // Cleanup removes the temporary directory (unless the --skip-cleanup flag was set) | 
|  | // It is safe to call cleanup multiple times. | 
|  | func (e *Exported) Cleanup() { | 
|  | if e.temp == "" { | 
|  | return | 
|  | } | 
|  | if *skipCleanup { | 
|  | log.Printf("Skipping cleanup of temp dir: %s", e.temp) | 
|  | return | 
|  | } | 
|  | // Make everything read-write so that the Module exporter's module cache can be deleted. | 
|  | filepath.Walk(e.temp, func(path string, info os.FileInfo, err error) error { | 
|  | if err != nil { | 
|  | return nil | 
|  | } | 
|  | if info.IsDir() { | 
|  | os.Chmod(path, 0777) | 
|  | } | 
|  | return nil | 
|  | }) | 
|  | os.RemoveAll(e.temp) // ignore errors | 
|  | e.temp = "" | 
|  | } | 
|  |  | 
|  | // Temp returns the temporary directory that was generated. | 
|  | func (e *Exported) Temp() string { | 
|  | return e.temp | 
|  | } | 
|  |  | 
|  | // File returns the full path for the given module and file fragment. | 
|  | func (e *Exported) File(module, fragment string) string { | 
|  | if m := e.written[module]; m != nil { | 
|  | return m[fragment] | 
|  | } | 
|  | return "" | 
|  | } | 
|  |  | 
|  | // FileContents returns the contents of the specified file. | 
|  | // It will use the overlay if the file is present, otherwise it will read it | 
|  | // from disk. | 
|  | func (e *Exported) FileContents(filename string) ([]byte, error) { | 
|  | if content, found := e.Config.Overlay[filename]; found { | 
|  | return content, nil | 
|  | } | 
|  | content, err := ioutil.ReadFile(filename) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | return content, nil | 
|  | } |