...

Source file src/golang.org/x/tools/go/packages/packagestest/export.go

Documentation: golang.org/x/tools/go/packages/packagestest

     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  }
   665  

View as plain text