gopkg.in/alecthomas/gometalinter.v3@v3.0.0/_linters/src/golang.org/x/tools/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  package packagestest
    13  
    14  import (
    15  	"flag"
    16  	"fmt"
    17  	"go/token"
    18  	"io/ioutil"
    19  	"log"
    20  	"os"
    21  	"path/filepath"
    22  	"strings"
    23  	"testing"
    24  
    25  	"golang.org/x/tools/go/expect"
    26  	"golang.org/x/tools/go/packages"
    27  )
    28  
    29  var (
    30  	skipCleanup = flag.Bool("skip-cleanup", false, "Do not delete the temporary export folders") // for debugging
    31  )
    32  
    33  // Module is a representation of a go module.
    34  type Module struct {
    35  	// Name is the base name of the module as it would be in the go.mod file.
    36  	Name string
    37  	// Files is the set of source files for all packages that make up the module.
    38  	// The keys are the file fragment that follows the module name, the value can
    39  	// be a string or byte slice, in which case it is the contents of the
    40  	// file, otherwise it must be a Writer function.
    41  	Files map[string]interface{}
    42  
    43  	// Overlay is the set of source file overlays for the module.
    44  	// The keys are the file fragment as in the Files configuration.
    45  	// The values are the in memory overlay content for the file.
    46  	Overlay map[string][]byte
    47  }
    48  
    49  // A Writer is a function that writes out a test file.
    50  // It is provided the name of the file to write, and may return an error if it
    51  // cannot write the file.
    52  // These are used as the content of the Files map in a Module.
    53  type Writer func(filename string) error
    54  
    55  // Exported is returned by the Export function to report the structure that was produced on disk.
    56  type Exported struct {
    57  	// Config is a correctly configured packages.Config ready to be passed to packages.Load.
    58  	// Exactly what it will contain varies depending on the Exporter being used.
    59  	Config *packages.Config
    60  
    61  	// Modules is the module description that was used to produce this exported data set.
    62  	Modules []Module
    63  
    64  	temp    string                       // the temporary directory that was exported to
    65  	primary string                       // the first non GOROOT module that was exported
    66  	written map[string]map[string]string // the full set of exported files
    67  	fset    *token.FileSet               // The file set used when parsing expectations
    68  	notes   []*expect.Note               // The list of expectations extracted from go source files
    69  	markers map[string]Range             // The set of markers extracted from go source files
    70  }
    71  
    72  // Exporter implementations are responsible for converting from the generic description of some
    73  // test data to a driver specific file layout.
    74  type Exporter interface {
    75  	// Name reports the name of the exporter, used in logging and sub-test generation.
    76  	Name() string
    77  	// Filename reports the system filename for test data source file.
    78  	// It is given the base directory, the module the file is part of and the filename fragment to
    79  	// work from.
    80  	Filename(exported *Exported, module, fragment string) string
    81  	// Finalize is called once all files have been written to write any extra data needed and modify
    82  	// the Config to match. It is handed the full list of modules that were encountered while writing
    83  	// files.
    84  	Finalize(exported *Exported) error
    85  }
    86  
    87  // All is the list of known exporters.
    88  // This is used by TestAll to run tests with all the exporters.
    89  var All []Exporter
    90  
    91  // TestAll invokes the testing function once for each exporter registered in
    92  // the All global.
    93  // Each exporter will be run as a sub-test named after the exporter being used.
    94  func TestAll(t *testing.T, f func(*testing.T, Exporter)) {
    95  	t.Helper()
    96  	for _, e := range All {
    97  		t.Run(e.Name(), func(t *testing.T) {
    98  			t.Helper()
    99  			f(t, e)
   100  		})
   101  	}
   102  }
   103  
   104  // BenchmarkAll invokes the testing function once for each exporter registered in
   105  // the All global.
   106  // Each exporter will be run as a sub-test named after the exporter being used.
   107  func BenchmarkAll(b *testing.B, f func(*testing.B, Exporter)) {
   108  	b.Helper()
   109  	for _, e := range All {
   110  		b.Run(e.Name(), func(b *testing.B) {
   111  			b.Helper()
   112  			f(b, e)
   113  		})
   114  	}
   115  }
   116  
   117  // Export is called to write out a test directory from within a test function.
   118  // It takes the exporter and the build system agnostic module descriptions, and
   119  // uses them to build a temporary directory.
   120  // It returns an Exported with the results of the export.
   121  // The Exported.Config is prepared for loading from the exported data.
   122  // You must invoke Exported.Cleanup on the returned value to clean up.
   123  // The file deletion in the cleanup can be skipped by setting the skip-cleanup
   124  // flag when invoking the test, allowing the temporary directory to be left for
   125  // debugging tests.
   126  func Export(t testing.TB, exporter Exporter, modules []Module) *Exported {
   127  	t.Helper()
   128  	dirname := strings.Replace(t.Name(), "/", "_", -1)
   129  	dirname = strings.Replace(dirname, "#", "_", -1) // duplicate subtests get a #NNN suffix.
   130  	temp, err := ioutil.TempDir("", dirname)
   131  	if err != nil {
   132  		t.Fatal(err)
   133  	}
   134  	exported := &Exported{
   135  		Config: &packages.Config{
   136  			Dir:     temp,
   137  			Env:     append(os.Environ(), "GOPACKAGESDRIVER=off"),
   138  			Overlay: make(map[string][]byte),
   139  		},
   140  		Modules: modules,
   141  		temp:    temp,
   142  		primary: modules[0].Name,
   143  		written: map[string]map[string]string{},
   144  		fset:    token.NewFileSet(),
   145  	}
   146  	defer func() {
   147  		if t.Failed() || t.Skipped() {
   148  			exported.Cleanup()
   149  		}
   150  	}()
   151  	for _, module := range modules {
   152  		for fragment, value := range module.Files {
   153  			fullpath := exporter.Filename(exported, module.Name, filepath.FromSlash(fragment))
   154  			written, ok := exported.written[module.Name]
   155  			if !ok {
   156  				written = map[string]string{}
   157  				exported.written[module.Name] = written
   158  			}
   159  			written[fragment] = fullpath
   160  			if err := os.MkdirAll(filepath.Dir(fullpath), 0755); err != nil {
   161  				t.Fatal(err)
   162  			}
   163  			switch value := value.(type) {
   164  			case Writer:
   165  				if err := value(fullpath); err != nil {
   166  					t.Fatal(err)
   167  				}
   168  			case string:
   169  				if err := ioutil.WriteFile(fullpath, []byte(value), 0644); err != nil {
   170  					t.Fatal(err)
   171  				}
   172  			default:
   173  				t.Fatalf("Invalid type %T in files, must be string or Writer", value)
   174  			}
   175  		}
   176  		for fragment, value := range module.Overlay {
   177  			fullpath := exporter.Filename(exported, module.Name, filepath.FromSlash(fragment))
   178  			exported.Config.Overlay[fullpath] = value
   179  		}
   180  	}
   181  	if err := exporter.Finalize(exported); err != nil {
   182  		t.Fatal(err)
   183  	}
   184  	return exported
   185  }
   186  
   187  // Script returns a Writer that writes out contents to the file and sets the
   188  // executable bit on the created file.
   189  // It is intended for source files that are shell scripts.
   190  func Script(contents string) Writer {
   191  	return func(filename string) error {
   192  		return ioutil.WriteFile(filename, []byte(contents), 0755)
   193  	}
   194  }
   195  
   196  // Link returns a Writer that creates a hard link from the specified source to
   197  // the required file.
   198  // This is used to link testdata files into the generated testing tree.
   199  func Link(source string) Writer {
   200  	return func(filename string) error {
   201  		return os.Link(source, filename)
   202  	}
   203  }
   204  
   205  // Symlink returns a Writer that creates a symlink from the specified source to the
   206  // required file.
   207  // This is used to link testdata files into the generated testing tree.
   208  func Symlink(source string) Writer {
   209  	if !strings.HasPrefix(source, ".") {
   210  		if abspath, err := filepath.Abs(source); err == nil {
   211  			if _, err := os.Stat(source); !os.IsNotExist(err) {
   212  				source = abspath
   213  			}
   214  		}
   215  	}
   216  	return func(filename string) error {
   217  		return os.Symlink(source, filename)
   218  	}
   219  }
   220  
   221  // Copy returns a Writer that copies a file from the specified source to the
   222  // required file.
   223  // This is used to copy testdata files into the generated testing tree.
   224  func Copy(source string) Writer {
   225  	return func(filename string) error {
   226  		stat, err := os.Stat(source)
   227  		if err != nil {
   228  			return err
   229  		}
   230  		if !stat.Mode().IsRegular() {
   231  			// cannot copy non-regular files (e.g., directories,
   232  			// symlinks, devices, etc.)
   233  			return fmt.Errorf("Cannot copy non regular file %s", source)
   234  		}
   235  		contents, err := ioutil.ReadFile(source)
   236  		if err != nil {
   237  			return err
   238  		}
   239  		return ioutil.WriteFile(filename, contents, stat.Mode())
   240  	}
   241  }
   242  
   243  // MustCopyFileTree returns a file set for a module based on a real directory tree.
   244  // It scans the directory tree anchored at root and adds a Copy writer to the
   245  // map for every file found.
   246  // This is to enable the common case in tests where you have a full copy of the
   247  // package in your testdata.
   248  // This will panic if there is any kind of error trying to walk the file tree.
   249  func MustCopyFileTree(root string) map[string]interface{} {
   250  	result := map[string]interface{}{}
   251  	if err := filepath.Walk(filepath.FromSlash(root), func(path string, info os.FileInfo, err error) error {
   252  		if err != nil {
   253  			return err
   254  		}
   255  		if info.IsDir() {
   256  			return nil
   257  		}
   258  		fragment, err := filepath.Rel(root, path)
   259  		if err != nil {
   260  			return err
   261  		}
   262  		result[fragment] = Copy(path)
   263  		return nil
   264  	}); err != nil {
   265  		log.Panic(fmt.Sprintf("MustCopyFileTree failed: %v", err))
   266  	}
   267  	return result
   268  }
   269  
   270  // Cleanup removes the temporary directory (unless the --skip-cleanup flag was set)
   271  // It is safe to call cleanup multiple times.
   272  func (e *Exported) Cleanup() {
   273  	if e.temp == "" {
   274  		return
   275  	}
   276  	if *skipCleanup {
   277  		log.Printf("Skipping cleanup of temp dir: %s", e.temp)
   278  		return
   279  	}
   280  	// Make everything read-write so that the Module exporter's module cache can be deleted.
   281  	filepath.Walk(e.temp, func(path string, info os.FileInfo, err error) error {
   282  		if err != nil {
   283  			return nil
   284  		}
   285  		if info.IsDir() {
   286  			os.Chmod(path, 0777)
   287  		}
   288  		return nil
   289  	})
   290  	os.RemoveAll(e.temp) // ignore errors
   291  	e.temp = ""
   292  }
   293  
   294  // Temp returns the temporary directory that was generated.
   295  func (e *Exported) Temp() string {
   296  	return e.temp
   297  }
   298  
   299  // File returns the full path for the given module and file fragment.
   300  func (e *Exported) File(module, fragment string) string {
   301  	if m := e.written[module]; m != nil {
   302  		return m[fragment]
   303  	}
   304  	return ""
   305  }
   306  
   307  // FileContents returns the contents of the specified file.
   308  // It will use the overlay if the file is present, otherwise it will read it
   309  // from disk.
   310  func (e *Exported) FileContents(filename string) ([]byte, error) {
   311  	if content, found := e.Config.Overlay[filename]; found {
   312  		return content, nil
   313  	}
   314  	content, err := ioutil.ReadFile(filename)
   315  	if err != nil {
   316  		return nil, err
   317  	}
   318  	return content, nil
   319  }