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.
6 Package packagestest creates temporary projects on disk for testing go tools on.
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
14 As an example of packagestest use, consider the following test that runs
15 the 'go list' command on the specified modules:
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{
22 Name: "gopher.example/repoa",
23 Files: map[string]interface{}{
24 "a/a.go": "package a",
28 Name: "gopher.example/repob",
29 Files: map[string]interface{}{
30 "b/b.go": "package b",
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()
43 t.Logf("'go list gopher.example/...' with %s mode layout:\n%s", x.Name(), out)
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:
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
78 "golang.org/x/tools/go/expect"
79 "golang.org/x/tools/go/packages"
80 "golang.org/x/tools/internal/span"
81 "golang.org/x/tools/internal/testenv"
85 skipCleanup = flag.Bool("skip-cleanup", false, "Do not delete the temporary export folders") // for debugging
88 // Module is a representation of a go module.
90 // Name is the base name of the module as it would be in the go.mod file.
92 // Files is the set of source files for all packages that make up the module.
93 // The keys are the file fragment that follows the module name, the value can
94 // be a string or byte slice, in which case it is the contents of the
95 // file, otherwise it must be a Writer function.
96 Files map[string]interface{}
98 // Overlay is the set of source file overlays for the module.
99 // The keys are the file fragment as in the Files configuration.
100 // The values are the in memory overlay content for the file.
101 Overlay map[string][]byte
104 // A Writer is a function that writes out a test file.
105 // It is provided the name of the file to write, and may return an error if it
106 // cannot write the file.
107 // These are used as the content of the Files map in a Module.
108 type Writer func(filename string) error
110 // Exported is returned by the Export function to report the structure that was produced on disk.
111 type Exported struct {
112 // Config is a correctly configured packages.Config ready to be passed to packages.Load.
113 // Exactly what it will contain varies depending on the Exporter being used.
114 Config *packages.Config
116 // Modules is the module description that was used to produce this exported data set.
119 ExpectFileSet *token.FileSet // The file set used when parsing expectations
121 Exporter Exporter // the exporter used
122 temp string // the temporary directory that was exported to
123 primary string // the first non GOROOT module that was exported
124 written map[string]map[string]string // the full set of exported files
125 notes []*expect.Note // The list of expectations extracted from go source files
126 markers map[string]span.Range // The set of markers extracted from go source files
129 // Exporter implementations are responsible for converting from the generic description of some
130 // test data to a driver specific file layout.
131 type Exporter interface {
132 // Name reports the name of the exporter, used in logging and sub-test generation.
134 // Filename reports the system filename for test data source file.
135 // It is given the base directory, the module the file is part of and the filename fragment to
137 Filename(exported *Exported, module, fragment string) string
138 // Finalize is called once all files have been written to write any extra data needed and modify
139 // the Config to match. It is handed the full list of modules that were encountered while writing
141 Finalize(exported *Exported) error
144 // All is the list of known exporters.
145 // This is used by TestAll to run tests with all the exporters.
148 // TestAll invokes the testing function once for each exporter registered in
150 // Each exporter will be run as a sub-test named after the exporter being used.
151 func TestAll(t *testing.T, f func(*testing.T, Exporter)) {
153 for _, e := range All {
154 t.Run(e.Name(), func(t *testing.T) {
161 // BenchmarkAll invokes the testing function once for each exporter registered in
163 // Each exporter will be run as a sub-test named after the exporter being used.
164 func BenchmarkAll(b *testing.B, f func(*testing.B, Exporter)) {
166 for _, e := range All {
167 b.Run(e.Name(), func(b *testing.B) {
174 // Export is called to write out a test directory from within a test function.
175 // It takes the exporter and the build system agnostic module descriptions, and
176 // uses them to build a temporary directory.
177 // It returns an Exported with the results of the export.
178 // The Exported.Config is prepared for loading from the exported data.
179 // You must invoke Exported.Cleanup on the returned value to clean up.
180 // The file deletion in the cleanup can be skipped by setting the skip-cleanup
181 // flag when invoking the test, allowing the temporary directory to be left for
183 func Export(t testing.TB, exporter Exporter, modules []Module) *Exported {
185 if exporter == Modules {
186 testenv.NeedsTool(t, "go")
189 dirname := strings.Replace(t.Name(), "/", "_", -1)
190 dirname = strings.Replace(dirname, "#", "_", -1) // duplicate subtests get a #NNN suffix.
191 temp, err := ioutil.TempDir("", dirname)
195 exported := &Exported{
196 Config: &packages.Config{
198 Env: append(os.Environ(), "GOPACKAGESDRIVER=off", "GOROOT="), // Clear GOROOT to work around #32849.
199 Overlay: make(map[string][]byte),
201 Mode: packages.LoadImports,
206 primary: modules[0].Name,
207 written: map[string]map[string]string{},
208 ExpectFileSet: token.NewFileSet(),
211 if t.Failed() || t.Skipped() {
215 for _, module := range modules {
216 for fragment, value := range module.Files {
217 fullpath := exporter.Filename(exported, module.Name, filepath.FromSlash(fragment))
218 written, ok := exported.written[module.Name]
220 written = map[string]string{}
221 exported.written[module.Name] = written
223 written[fragment] = fullpath
224 if err := os.MkdirAll(filepath.Dir(fullpath), 0755); err != nil {
227 switch value := value.(type) {
229 if err := value(fullpath); err != nil {
233 if err := ioutil.WriteFile(fullpath, []byte(value), 0644); err != nil {
237 t.Fatalf("Invalid type %T in files, must be string or Writer", value)
240 for fragment, value := range module.Overlay {
241 fullpath := exporter.Filename(exported, module.Name, filepath.FromSlash(fragment))
242 exported.Config.Overlay[fullpath] = value
245 if err := exporter.Finalize(exported); err != nil {
248 testenv.NeedsGoPackagesEnv(t, exported.Config.Env)
252 // Script returns a Writer that writes out contents to the file and sets the
253 // executable bit on the created file.
254 // It is intended for source files that are shell scripts.
255 func Script(contents string) Writer {
256 return func(filename string) error {
257 return ioutil.WriteFile(filename, []byte(contents), 0755)
261 // Link returns a Writer that creates a hard link from the specified source to
262 // the required file.
263 // This is used to link testdata files into the generated testing tree.
264 func Link(source string) Writer {
265 return func(filename string) error {
266 return os.Link(source, filename)
270 // Symlink returns a Writer that creates a symlink from the specified source to the
272 // This is used to link testdata files into the generated testing tree.
273 func Symlink(source string) Writer {
274 if !strings.HasPrefix(source, ".") {
275 if abspath, err := filepath.Abs(source); err == nil {
276 if _, err := os.Stat(source); !os.IsNotExist(err) {
281 return func(filename string) error {
282 return os.Symlink(source, filename)
286 // Copy returns a Writer that copies a file from the specified source to the
288 // This is used to copy testdata files into the generated testing tree.
289 func Copy(source string) Writer {
290 return func(filename string) error {
291 stat, err := os.Stat(source)
295 if !stat.Mode().IsRegular() {
296 // cannot copy non-regular files (e.g., directories,
297 // symlinks, devices, etc.)
298 return fmt.Errorf("cannot copy non regular file %s", source)
300 contents, err := ioutil.ReadFile(source)
304 return ioutil.WriteFile(filename, contents, stat.Mode())
308 // GroupFilesByModules attempts to map directories to the modules within each directory.
309 // This function assumes that the folder is structured in the following way:
314 // - go.mod (optional)
320 // - go.mod (optional)
321 // It scans the directory tree anchored at root and adds a Copy writer to the
322 // map for every file found.
323 // This is to enable the common case in tests where you have a full copy of the
324 // package in your testdata.
325 func GroupFilesByModules(root string) ([]Module, error) {
326 root = filepath.FromSlash(root)
327 primarymodPath := filepath.Join(root, "primarymod")
329 _, err := os.Stat(primarymodPath)
330 if os.IsNotExist(err) {
331 return nil, fmt.Errorf("could not find primarymod folder within %s", root)
334 primarymod := &Module{
336 Files: make(map[string]interface{}),
337 Overlay: make(map[string][]byte),
339 mods := map[string]*Module{
342 modules := []Module{*primarymod}
344 if err := filepath.Walk(primarymodPath, func(path string, info os.FileInfo, err error) error {
351 fragment, err := filepath.Rel(primarymodPath, path)
355 primarymod.Files[filepath.ToSlash(fragment)] = Copy(path)
361 modulesPath := filepath.Join(root, "modules")
362 if _, err := os.Stat(modulesPath); os.IsNotExist(err) {
366 var currentRepo, currentModule string
367 updateCurrentModule := func(dir string) {
368 if dir == currentModule {
371 // Handle the case where we step into a nested directory that is a module
372 // and then step out into the parent which is also a module.
382 if mods[dir] != nil {
386 dir = filepath.Dir(dir)
390 if err := filepath.Walk(modulesPath, func(path string, info os.FileInfo, err error) error {
394 enclosingDir := filepath.Dir(path)
395 // If the path is not a directory, then we want to add the path to
396 // the files map of the currentModule.
398 updateCurrentModule(enclosingDir)
399 fragment, err := filepath.Rel(currentModule, path)
403 mods[currentModule].Files[filepath.ToSlash(fragment)] = Copy(path)
406 // If the path is a directory and it's enclosing folder is equal to
407 // the modules folder, then the path is a new repo.
408 if enclosingDir == modulesPath {
412 // If the path is a directory and it's enclosing folder is not the same
413 // as the current repo and it is not of the form `v1`,`v2`,...
414 // then the path is a folder/package of the current module.
415 if enclosingDir != currentRepo && !versionSuffixRE.MatchString(filepath.Base(path)) {
418 // If the path is a directory and it's enclosing folder is the current repo
419 // then the path is a new module.
420 module, err := filepath.Rel(modulesPath, path)
424 mods[path] = &Module{
425 Name: filepath.ToSlash(module),
426 Files: make(map[string]interface{}),
427 Overlay: make(map[string][]byte),
430 modules = append(modules, *mods[path])
438 // MustCopyFileTree returns a file set for a module based on a real directory tree.
439 // It scans the directory tree anchored at root and adds a Copy writer to the
440 // map for every file found.
441 // This is to enable the common case in tests where you have a full copy of the
442 // package in your testdata.
443 // This will panic if there is any kind of error trying to walk the file tree.
444 func MustCopyFileTree(root string) map[string]interface{} {
445 result := map[string]interface{}{}
446 if err := filepath.Walk(filepath.FromSlash(root), func(path string, info os.FileInfo, err error) error {
453 fragment, err := filepath.Rel(root, path)
457 result[filepath.ToSlash(fragment)] = Copy(path)
460 log.Panic(fmt.Sprintf("MustCopyFileTree failed: %v", err))
465 // Cleanup removes the temporary directory (unless the --skip-cleanup flag was set)
466 // It is safe to call cleanup multiple times.
467 func (e *Exported) Cleanup() {
472 log.Printf("Skipping cleanup of temp dir: %s", e.temp)
475 // Make everything read-write so that the Module exporter's module cache can be deleted.
476 filepath.Walk(e.temp, func(path string, info os.FileInfo, err error) error {
485 os.RemoveAll(e.temp) // ignore errors
489 // Temp returns the temporary directory that was generated.
490 func (e *Exported) Temp() string {
494 // File returns the full path for the given module and file fragment.
495 func (e *Exported) File(module, fragment string) string {
496 if m := e.written[module]; m != nil {
502 // FileContents returns the contents of the specified file.
503 // It will use the overlay if the file is present, otherwise it will read it
505 func (e *Exported) FileContents(filename string) ([]byte, error) {
506 if content, found := e.Config.Overlay[filename]; found {
509 content, err := ioutil.ReadFile(filename)