golang.org/x/tools/gopls@v0.15.3/internal/test/integration/fake/sandbox.go (about)

     1  // Copyright 2020 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  package fake
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  
    15  	"golang.org/x/tools/internal/gocommand"
    16  	"golang.org/x/tools/internal/robustio"
    17  	"golang.org/x/tools/internal/testenv"
    18  	"golang.org/x/tools/txtar"
    19  )
    20  
    21  // Sandbox holds a collection of temporary resources to use for working with Go
    22  // code in tests.
    23  type Sandbox struct {
    24  	gopath          string
    25  	rootdir         string
    26  	goproxy         string
    27  	Workdir         *Workdir
    28  	goCommandRunner gocommand.Runner
    29  }
    30  
    31  // SandboxConfig controls the behavior of a test sandbox. The zero value
    32  // defines a reasonable default.
    33  type SandboxConfig struct {
    34  	// RootDir sets the base directory to use when creating temporary
    35  	// directories. If not specified, defaults to a new temporary directory.
    36  	RootDir string
    37  	// Files holds a txtar-encoded archive of files to populate the initial state
    38  	// of the working directory.
    39  	//
    40  	// For convenience, the special substring "$SANDBOX_WORKDIR" is replaced with
    41  	// the sandbox's resolved working directory before writing files.
    42  	Files map[string][]byte
    43  	// InGoPath specifies that the working directory should be within the
    44  	// temporary GOPATH.
    45  	InGoPath bool
    46  	// Workdir configures the working directory of the Sandbox. It behaves as
    47  	// follows:
    48  	//  - if set to an absolute path, use that path as the working directory.
    49  	//  - if set to a relative path, create and use that path relative to the
    50  	//    sandbox.
    51  	//  - if unset, default to a the 'work' subdirectory of the sandbox.
    52  	//
    53  	// This option is incompatible with InGoPath or Files.
    54  	Workdir string
    55  	// ProxyFiles holds a txtar-encoded archive of files to populate a file-based
    56  	// Go proxy.
    57  	ProxyFiles map[string][]byte
    58  	// GOPROXY is the explicit GOPROXY value that should be used for the sandbox.
    59  	//
    60  	// This option is incompatible with ProxyFiles.
    61  	GOPROXY string
    62  }
    63  
    64  // NewSandbox creates a collection of named temporary resources, with a
    65  // working directory populated by the txtar-encoded content in srctxt, and a
    66  // file-based module proxy populated with the txtar-encoded content in
    67  // proxytxt.
    68  //
    69  // If rootDir is non-empty, it will be used as the root of temporary
    70  // directories created for the sandbox. Otherwise, a new temporary directory
    71  // will be used as root.
    72  //
    73  // TODO(rfindley): the sandbox abstraction doesn't seem to carry its weight.
    74  // Sandboxes should be composed out of their building-blocks, rather than via a
    75  // monolithic configuration.
    76  func NewSandbox(config *SandboxConfig) (_ *Sandbox, err error) {
    77  	if config == nil {
    78  		config = new(SandboxConfig)
    79  	}
    80  	if err := validateConfig(*config); err != nil {
    81  		return nil, fmt.Errorf("invalid SandboxConfig: %v", err)
    82  	}
    83  
    84  	sb := &Sandbox{}
    85  	defer func() {
    86  		// Clean up if we fail at any point in this constructor.
    87  		if err != nil {
    88  			sb.Close()
    89  		}
    90  	}()
    91  
    92  	rootDir := config.RootDir
    93  	if rootDir == "" {
    94  		rootDir, err = os.MkdirTemp(config.RootDir, "gopls-sandbox-")
    95  		if err != nil {
    96  			return nil, fmt.Errorf("creating temporary workdir: %v", err)
    97  		}
    98  	}
    99  	sb.rootdir = rootDir
   100  	sb.gopath = filepath.Join(sb.rootdir, "gopath")
   101  	if err := os.Mkdir(sb.gopath, 0755); err != nil {
   102  		return nil, err
   103  	}
   104  	if config.GOPROXY != "" {
   105  		sb.goproxy = config.GOPROXY
   106  	} else {
   107  		proxydir := filepath.Join(sb.rootdir, "proxy")
   108  		if err := os.Mkdir(proxydir, 0755); err != nil {
   109  			return nil, err
   110  		}
   111  		sb.goproxy, err = WriteProxy(proxydir, config.ProxyFiles)
   112  		if err != nil {
   113  			return nil, err
   114  		}
   115  	}
   116  	// Short-circuit writing the workdir if we're given an absolute path, since
   117  	// this is used for running in an existing directory.
   118  	// TODO(findleyr): refactor this to be less of a workaround.
   119  	if filepath.IsAbs(config.Workdir) {
   120  		sb.Workdir, err = NewWorkdir(config.Workdir, nil)
   121  		if err != nil {
   122  			return nil, err
   123  		}
   124  		return sb, nil
   125  	}
   126  	var workdir string
   127  	if config.Workdir == "" {
   128  		if config.InGoPath {
   129  			// Set the working directory as $GOPATH/src.
   130  			workdir = filepath.Join(sb.gopath, "src")
   131  		} else if workdir == "" {
   132  			workdir = filepath.Join(sb.rootdir, "work")
   133  		}
   134  	} else {
   135  		// relative path
   136  		workdir = filepath.Join(sb.rootdir, config.Workdir)
   137  	}
   138  	if err := os.MkdirAll(workdir, 0755); err != nil {
   139  		return nil, err
   140  	}
   141  	sb.Workdir, err = NewWorkdir(workdir, config.Files)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	return sb, nil
   146  }
   147  
   148  // Tempdir creates a new temp directory with the given txtar-encoded files. It
   149  // is the responsibility of the caller to call os.RemoveAll on the returned
   150  // file path when it is no longer needed.
   151  func Tempdir(files map[string][]byte) (string, error) {
   152  	dir, err := os.MkdirTemp("", "gopls-tempdir-")
   153  	if err != nil {
   154  		return "", err
   155  	}
   156  	for name, data := range files {
   157  		if err := writeFileData(name, data, RelativeTo(dir)); err != nil {
   158  			return "", fmt.Errorf("writing to tempdir: %w", err)
   159  		}
   160  	}
   161  	return dir, nil
   162  }
   163  
   164  func UnpackTxt(txt string) map[string][]byte {
   165  	dataMap := make(map[string][]byte)
   166  	archive := txtar.Parse([]byte(txt))
   167  	for _, f := range archive.Files {
   168  		if _, ok := dataMap[f.Name]; ok {
   169  			panic(fmt.Sprintf("found file %q twice", f.Name))
   170  		}
   171  		dataMap[f.Name] = f.Data
   172  	}
   173  	return dataMap
   174  }
   175  
   176  func validateConfig(config SandboxConfig) error {
   177  	if filepath.IsAbs(config.Workdir) && (len(config.Files) > 0 || config.InGoPath) {
   178  		return errors.New("absolute Workdir cannot be set in conjunction with Files or InGoPath")
   179  	}
   180  	if config.Workdir != "" && config.InGoPath {
   181  		return errors.New("Workdir cannot be set in conjunction with InGoPath")
   182  	}
   183  	if config.GOPROXY != "" && config.ProxyFiles != nil {
   184  		return errors.New("GOPROXY cannot be set in conjunction with ProxyFiles")
   185  	}
   186  	return nil
   187  }
   188  
   189  // splitModuleVersionPath extracts module information from files stored in the
   190  // directory structure modulePath@version/suffix.
   191  // For example:
   192  //
   193  //	splitModuleVersionPath("mod.com@v1.2.3/package") = ("mod.com", "v1.2.3", "package")
   194  func splitModuleVersionPath(path string) (modulePath, version, suffix string) {
   195  	parts := strings.Split(path, "/")
   196  	var modulePathParts []string
   197  	for i, p := range parts {
   198  		if strings.Contains(p, "@") {
   199  			mv := strings.SplitN(p, "@", 2)
   200  			modulePathParts = append(modulePathParts, mv[0])
   201  			return strings.Join(modulePathParts, "/"), mv[1], strings.Join(parts[i+1:], "/")
   202  		}
   203  		modulePathParts = append(modulePathParts, p)
   204  	}
   205  	// Default behavior: this is just a module path.
   206  	return path, "", ""
   207  }
   208  
   209  func (sb *Sandbox) RootDir() string {
   210  	return sb.rootdir
   211  }
   212  
   213  // GOPATH returns the value of the Sandbox GOPATH.
   214  func (sb *Sandbox) GOPATH() string {
   215  	return sb.gopath
   216  }
   217  
   218  // GoEnv returns the default environment variables that can be used for
   219  // invoking Go commands in the sandbox.
   220  func (sb *Sandbox) GoEnv() map[string]string {
   221  	vars := map[string]string{
   222  		"GOPATH":           sb.GOPATH(),
   223  		"GOPROXY":          sb.goproxy,
   224  		"GO111MODULE":      "",
   225  		"GOSUMDB":          "off",
   226  		"GOPACKAGESDRIVER": "off",
   227  	}
   228  	if testenv.Go1Point() >= 5 {
   229  		vars["GOMODCACHE"] = ""
   230  	}
   231  	return vars
   232  }
   233  
   234  // goCommandInvocation returns a new gocommand.Invocation initialized with the
   235  // sandbox environment variables and working directory.
   236  func (sb *Sandbox) goCommandInvocation() gocommand.Invocation {
   237  	var vars []string
   238  	for k, v := range sb.GoEnv() {
   239  		vars = append(vars, fmt.Sprintf("%s=%s", k, v))
   240  	}
   241  	inv := gocommand.Invocation{
   242  		Env: vars,
   243  	}
   244  	// sb.Workdir may be nil if we exited the constructor with errors (we call
   245  	// Close to clean up any partial state from the constructor, which calls
   246  	// RunGoCommand).
   247  	if sb.Workdir != nil {
   248  		inv.WorkingDir = string(sb.Workdir.RelativeTo)
   249  	}
   250  	return inv
   251  }
   252  
   253  // RunGoCommand executes a go command in the sandbox. If checkForFileChanges is
   254  // true, the sandbox scans the working directory and emits file change events
   255  // for any file changes it finds.
   256  func (sb *Sandbox) RunGoCommand(ctx context.Context, dir, verb string, args, env []string, checkForFileChanges bool) error {
   257  	inv := sb.goCommandInvocation()
   258  	inv.Verb = verb
   259  	inv.Args = args
   260  	inv.Env = append(inv.Env, env...)
   261  	if dir != "" {
   262  		inv.WorkingDir = sb.Workdir.AbsPath(dir)
   263  	}
   264  	stdout, stderr, _, err := sb.goCommandRunner.RunRaw(ctx, inv)
   265  	if err != nil {
   266  		return fmt.Errorf("go command failed (stdout: %s) (stderr: %s): %v", stdout.String(), stderr.String(), err)
   267  	}
   268  	// Since running a go command may result in changes to workspace files,
   269  	// check if we need to send any "watched" file events.
   270  	//
   271  	// TODO(rFindley): this side-effect can impact the usability of the sandbox
   272  	//                 for benchmarks. Consider refactoring.
   273  	if sb.Workdir != nil && checkForFileChanges {
   274  		if err := sb.Workdir.CheckForFileChanges(ctx); err != nil {
   275  			return fmt.Errorf("checking for file changes: %w", err)
   276  		}
   277  	}
   278  	return nil
   279  }
   280  
   281  // GoVersion checks the version of the go command.
   282  // It returns the X in Go 1.X.
   283  func (sb *Sandbox) GoVersion(ctx context.Context) (int, error) {
   284  	inv := sb.goCommandInvocation()
   285  	return gocommand.GoVersion(ctx, inv, &sb.goCommandRunner)
   286  }
   287  
   288  // Close removes all state associated with the sandbox.
   289  func (sb *Sandbox) Close() error {
   290  	var goCleanErr error
   291  	if sb.gopath != "" {
   292  		// Important: run this command in RootDir so that it doesn't interact with
   293  		// any toolchain downloads that may occur
   294  		goCleanErr = sb.RunGoCommand(context.Background(), sb.RootDir(), "clean", []string{"-modcache"}, nil, false)
   295  	}
   296  	err := robustio.RemoveAll(sb.rootdir)
   297  	if err != nil || goCleanErr != nil {
   298  		return fmt.Errorf("error(s) cleaning sandbox: cleaning modcache: %v; removing files: %v", goCleanErr, err)
   299  	}
   300  	return nil
   301  }