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