--- /dev/null
+// 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
+}